<?php

// +----------------------------------------------------------------------
// | Chnxu
// +----------------------------------------------------------------------

declare (strict_types=1);

namespace app\databackup\command;

use think\admin\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Config;
use think\facade\Db;

/**
 * 备份数据库
 * @class DataBackup
 * @package app\databackup\command
 */
class DataBackup extends Command
{
    protected $config = [];
    protected $table_total = 0; // 总备份表个数
    protected $table_finish = 0; // 当前备份完成表个数

    /**
     * 配置消息指令
     */
    protected function configure()
    {
        $this->setName('chnxu:databackup');
        $this->setDescription('数据库备份指令');
    }

    /**
     * @param Input $input
     * @param Output $output
     * @throws \WeChat\Exceptions\InvalidResponseException
     * @throws \WeChat\Exceptions\LocalCacheException
     * @throws \think\admin\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    protected function execute(Input $input, Output $output)
    {
        @ini_set('memory_limit', '-1');
        $this->config = Config::load(root_path('plugin'.DIRECTORY_SEPARATOR.'think-plugs-databackup'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'config').'databackup.php', 'databackup');
        if (!is_dir($this->config['path'])) {
            mkdir($this->config['path'], 0755, true);
        }

        $tables = $this->_getTables();
        $this->table_total = count($tables);
        // 定义最终的备份文件名
        $backupFilename = $this->config['path'] . date('Ymd-His') . '.sql';
        $compress = $this->config['compress'] ?? 0; // 是否启用压缩
        $zipFilename = $backupFilename . '.gz'; // 压缩文件名
        $this->queue->message($this->table_total, 0, '开始备份数据库，共'.$this->table_total.'个表');
        foreach ($tables as $table_name) {
            $this->queue->message($this->table_total, ++$this->table_finish, "-> 开始备份 {$table_name}");
            $this->_backupTable($backupFilename, $table_name);
            $this->queue->message($this->table_total, $this->table_finish, "-> 完成备份 {$table_name}", 2);
        }

        if ($this->table_finish > 0) {
            $this->setQueueProgress('数据库备份完成');
            // 如果启用压缩，对文件进行压缩
            if ($compress) {
                $this->setQueueProgress('文件压缩中...');
                $this->_compressFile($backupFilename, $zipFilename);
                $this->_deleteFile($backupFilename); // 删除原始文件
                $this->setQueueProgress('文件压缩完成', '100', 1);
            }
            $this->setQueueSuccess(sprintf('共备份 %d 个表数据', $this->table_finish));
        } else {
            $this->setQueueSuccess('未备份任何数据');
        }
    }

    /**
     * 获取要备份的数据表
     *
     * @return array
     */
    private function _getTables(): array
    {
        $tables = $this->config['tables'] ?? [];
        $ignore = $this->config['ignore'] ?? [];
        if (empty($tables)) {
            foreach (Db::query("SHOW TABLES") as $table) {
                $tables[] = current($table);
            }
        }

        if (!empty($ignore)) {
            $tables = array_diff($tables, $ignore);
        }

        return $tables;
    }
    
    /**
     * 备份单个数据库表内容
     *
     * @param string $backupFilename 备份文件路径
     * @param string $tableName 表名
     * @return void
     */
    private function _backupTable(string $backupFilename, string $tableName)
    {
        $dropTable = "DROP TABLE IF EXISTS `{$tableName}`;\n";
        $createTable = Db::query("SHOW CREATE TABLE `{$tableName}`")[0]['Create Table'];

        file_put_contents($backupFilename, $dropTable.$createTable . ";\n", FILE_APPEND); // 追加内容到文件
        $total = Db::name($tableName)->count();
        $done = 0;
        $this->queue->message($this->table_total, $this->table_finish, "----> 共 0/{$total} 条数据");
        // 使用游标查询逐行处理数据
        $cursor = Db::name($tableName)->cursor();
        foreach ($cursor as $row) {
            $keys = array_keys($row);
            $values = array_map(function($value) {
                return "'" . addslashes((string)$value) . "'";
            }, $row);
            $insertSql = "INSERT INTO `{$tableName}` (`" . implode('`, `', $keys) . "`) VALUES (" . implode(',', $values) . ");\n";
            file_put_contents($backupFilename, $insertSql, FILE_APPEND); // 追加内容到文件
            ++$done;
            $this->queue->message($this->table_total, $this->table_finish, "----> 共 {$done}/{$total} 条数据", 1);
        }
    }

    /**
     * 压缩文件
     *
     * @param string $sourceFile 源文件
     * @param string $zipFilename 压缩文件名
     * @return void
     */
    private function _compressFile(string $sourceFile, string $zipFilename)
    {
        $content = file_get_contents($sourceFile);
        $compressedContent = gzencode($content, $this->config['level'] ?? 9); // 压缩内容
        file_put_contents($zipFilename, $compressedContent); // 写入压缩文件
    }

    /**
     * 删除文件
     *
     * @param string $filename 文件名
     * @return void
     */
    private function _deleteFile(string $filename)
    {
        if (file_exists($filename)) {
            unlink($filename);
        }
    }
}